package bsh.meta;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import bsh.CompilerImpl;

/**
 * {@link TypeWrapper} sobre classes java visiveis pelo classloader
 * 
 * @author takeshi
 */
public class JavaClassWrapper extends AbstractTypeWrapper implements TypeWrapper {

    public static final TypeWrapper BIG_DECIMAL_WRAPPER = new JavaClassWrapper(BigDecimal.class);

    public static final TypeWrapper OBJECT_WRAPPER = new JavaClassWrapper(Object.class);

    public static final TypeWrapper DOUBLE_WRAPPER = new JavaClassWrapper(double.class) {
        @Override
        public int numberOfSlots() {
            return 2;
        }

        public void allocatePrimitiveArray(MethodVisitor visitor) {
            visitor.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_DOUBLE);
        }

    };

    public static final TypeWrapper FLOAT_WRAPPER = new JavaClassWrapper(float.class) {
        public void allocatePrimitiveArray(MethodVisitor visitor) {
            visitor.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_FLOAT);
        }

    };

    public static final TypeWrapper LONG_WRAPPER = new JavaClassWrapper(long.class) {
        @Override
        public int numberOfSlots() {
            return 2;
        };

        public void allocatePrimitiveArray(MethodVisitor visitor) {
            visitor.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_LONG);
        }

    };

    public static final TypeWrapper SHORT_WRAPPER = new JavaClassWrapper(short.class) {
        public void allocatePrimitiveArray(MethodVisitor visitor) {
            visitor.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_SHORT);
        }
    };

    public static final TypeWrapper INT_WRAPPER = new JavaClassWrapper(int.class) {
        public void allocatePrimitiveArray(MethodVisitor visitor) {
            visitor.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_INT);
        }

    };

    public static final TypeWrapper BOOLEAN_WRAPPER = new JavaClassWrapper(boolean.class) {
        public void allocatePrimitiveArray(MethodVisitor visitor) {
            visitor.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_BOOLEAN);
        }

    };

    public static final TypeWrapper VOID_WRAPPER = new VoidTypeWrapper();

    public static final TypeWrapper CHAR_WRAPPER = new JavaClassWrapper(char.class) {
        public void allocatePrimitiveArray(MethodVisitor visitor) {
            visitor.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_CHAR);
        }
    };

    public static final TypeWrapper BYTE_WRAPPER = new JavaClassWrapper(byte.class) {
        public void allocatePrimitiveArray(MethodVisitor visitor) {
            visitor.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_BYTE);
        }
    };

    public static final TypeWrapper STRING_WRAPPER = new JavaClassWrapper(String.class);

    private static final Map<Class<?>, TypeWrapper> WRAPPER_CACHE = new HashMap<Class<?>, TypeWrapper>();
    static {
        WRAPPER_CACHE.put(Object.class, OBJECT_WRAPPER);
        WRAPPER_CACHE.put(double.class, DOUBLE_WRAPPER);
        WRAPPER_CACHE.put(float.class, FLOAT_WRAPPER);
        WRAPPER_CACHE.put(long.class, LONG_WRAPPER);
        WRAPPER_CACHE.put(short.class, SHORT_WRAPPER);
        WRAPPER_CACHE.put(int.class, INT_WRAPPER);
        WRAPPER_CACHE.put(boolean.class, BOOLEAN_WRAPPER);
        WRAPPER_CACHE.put(void.class, VOID_WRAPPER);
        WRAPPER_CACHE.put(char.class, CHAR_WRAPPER);
        WRAPPER_CACHE.put(byte.class, BYTE_WRAPPER);
        WRAPPER_CACHE.put(BigDecimal.class, BIG_DECIMAL_WRAPPER);
        WRAPPER_CACHE.put(String.class, STRING_WRAPPER);
    }

    private final Class<?> type;

    private final Type asmType;

    private final Collection<MethodDescriptor> methods;

    /**
     * @param type Class
     */
    protected JavaClassWrapper(Class<?> type) {
        super();
        this.type = type;
        this.asmType = Type.getType(type);
        this.methods = new HashSet<MethodDescriptor>();
        addMethodsFromClass(type, methods);
    }

    /**
     * {@inheritDoc}
     */
    public String className() {
        return this.type.getName();
    }

    /**
     * {@inheritDoc}
     */
    public List<? extends TypeWrapper> interfaces() {
        Class<?>[] interfaces = this.type.getInterfaces();
        Set<TypeWrapper> ifs = new HashSet<TypeWrapper>();
        for (Class<?> class1 : interfaces) {
            ifs.add(wrapperFor(class1));
        }
        TypeWrapper type2 = this.superType();
        if (type2 != null) {
            ifs.addAll(type2.interfaces());
        }
        return new ArrayList<TypeWrapper>(ifs);
    }

    /**
     * {@inheritDoc}
     */
    public String internalName() {
        if (this.type.isPrimitive()) {
            return this.typeDescription();
        }
        return this.asmType.getInternalName();
    }

    /**
     * {@inheritDoc}
     */
    public TypeWrapper superType() {
        if (this.type.isPrimitive()) {
            return null;
        }
        if (this.type.equals(Object.class)) {
            return null;
        }
        // top level interface interface
        if (this.type.getSuperclass() == null) {
            return JavaClassWrapper.OBJECT_WRAPPER;
        }
        return wrapperFor(this.type.getSuperclass());
    }

    /**
     * {@inheritDoc}
     */
    public String typeDescription() {
        return this.asmType.getDescriptor();
    }

    /**
     * {@inheritDoc}
     */
    public Collection<? extends FieldDescriptor> visibleFields() {
        Field[] fields = this.type.getFields();
        List<FieldDescriptor> lis = new ArrayList<FieldDescriptor>(fields.length);
        for (Field field : fields) {
            lis.add(new JavaFieldDescriptor(field));
        }
        return lis;
    }

    /**
     * {@inheritDoc}
     */
    public Collection<? extends MethodDescriptor> visibleMethods() {
        return new HashSet<MethodDescriptor>(this.methods);
    }

    /**
     * @param c the class
     * @param methods the Collection
     */
    private static void addMethodsFromClass(Class<?> c, Collection<? super MethodDescriptor> methods) {
        for (Method method : c.getMethods()) {
            if (Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
                methods.add(new JavaMethodDescriptor(method));
            }
        }
        Class[] interfaces = c.getInterfaces();
        for (Class if0 : interfaces) {
            addMethodsFromClass(if0, methods);
        }
        if (c.getSuperclass() != null) {
            addMethodsFromClass(c.getSuperclass(), methods);
        }
    }

    /**
     * {@inheritDoc}
     */
    public static TypeWrapper wrapperFor(Class<?> type) {
        if (type == void.class || type == Void.class) {
            return new VoidTypeWrapper();
        } else if (type.isArray()) {
            return new ArrayTypeWrapper(wrapperFor(type.getComponentType()));
        } else {
            TypeWrapper cached = WRAPPER_CACHE.get(type);
            if (cached != null) {
                return cached;
            }
            return new JavaClassWrapper(type);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((this.type == null) ? 0 : this.type.hashCode());
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (this.getClass() != obj.getClass()) {
            return false;
        }
        JavaClassWrapper other = (JavaClassWrapper) obj;
        if (this.type == null) {
            if (other.type != null) {
                return false;
            }
        } else if (!this.type.equals(other.type)) {
            return false;
        }
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return this.type.toString();
    }

    /**
     * {@inheritDoc}
     */
    public boolean isInterface() {
        return this.type.isInterface();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isPrimitive() {
        return this.type.isPrimitive();
    }

    /**
     * {@inheritDoc}
     */
    public Class<?> getJavaType() {
        return this.type;
    }

    /**
     * {@inheritDoc}
     */
    public static Class<?> unwrapJavaClass(TypeWrapper wrapper) {
        if (wrapper instanceof JavaClassWrapper) {
            return ((JavaClassWrapper) wrapper).getJavaType();
        }
        return null;
    }

    private static final List<TypeWrapper> NUMBER_WRAPPERS = Arrays.asList(BYTE_WRAPPER, SHORT_WRAPPER, CHAR_WRAPPER, INT_WRAPPER,
            LONG_WRAPPER, FLOAT_WRAPPER, DOUBLE_WRAPPER, BIG_DECIMAL_WRAPPER);

    /**
     * {@inheritDoc}
     */
    public static TypeWrapper largestNumberType(TypeWrapper left, TypeWrapper right) {
        int li = NUMBER_WRAPPERS.indexOf(left);
        if (li < 0) {
            return null;
        }
        int ri = NUMBER_WRAPPERS.indexOf(right);
        if (ri < 0) {
            return null;
        }
        if (li < ri) {
            return right;
        }
        return left;
    }

    /**
     * @param wrapper {@link TypeWrapper}
     * @return {@link TypeWrapper}
     */
    public static TypeWrapper unboxType(TypeWrapper wrapper) {
        Class<?> javaWrapper = unwrapJavaClass(wrapper);
        if (javaWrapper == null) {
            return null;
        }
        return wrapperFor(CompilerImpl.unboxType(javaWrapper));
    }

    /**
     * @param type {@link TypeWrapper}
     * @return boolean
     */
    public static boolean isPrimitive(TypeWrapper type) {
        return type instanceof JavaClassWrapper && ((JavaClassWrapper) type).getJavaType().isPrimitive();
    }

    /**
     * {@inheritDoc}
     */
    public Collection<? extends List<TypeWrapper>> visibleConstructors() {
        Constructor<?>[] constructors = this.type.getConstructors();
        List<List<TypeWrapper>> ctorParams = new ArrayList<List<TypeWrapper>>();
        for (Constructor<?> constructor : constructors) {
            Class<?>[] types = constructor.getParameterTypes();
            List<TypeWrapper> list = new ArrayList<TypeWrapper>();
            for (Class<?> class1 : types) {
                list.add(wrapperFor(class1));
            }
            ctorParams.add(list);
        }
        return ctorParams;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.meta.TypeWrapper#protectedFields()
     */
    public Collection<? extends FieldDescriptor> protectedFields() {
        final TypeWrapper superType = superType();
        final Collection<FieldDescriptor> collection;
        if (superType != null) {
            collection = new ArrayList<FieldDescriptor>(superType.protectedFields());
        } else {
            collection = new ArrayList<FieldDescriptor>();
        }
        Field[] fields = this.type.getDeclaredFields();
        for (Field field : fields) {
            if (Modifier.isProtected(field.getModifiers())) {
                collection.add(new JavaFieldDescriptor(field));
            }
        }
        return collection;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.meta.TypeWrapper#protectedMethods()
     */
    public Collection<? extends MethodDescriptor> protectedMethods() {
        final TypeWrapper superType = superType();
        final Collection<MethodDescriptor> ms;
        if (superType != null) {
            ms = new ArrayList<MethodDescriptor>(superType.protectedMethods());
        } else {
            ms = new ArrayList<MethodDescriptor>();
        }
        Method[] declaredMethods = this.type.getDeclaredMethods();
        for (Method method : declaredMethods) {
            if (Modifier.isProtected(method.getModifiers())) {
                ms.add(new JavaMethodDescriptor(method));
            }
        }
        return ms;
    }

    /**
     * Gera a instrucao para alocar um array
     * 
     * @param visitor {@link MethodVisitor}
     */
    public void allocatePrimitiveArray(MethodVisitor visitor) {
        visitor.visitTypeInsn(Opcodes.ANEWARRAY, this.internalName());
    }

    /**
     * {@inheritDoc}
     */
    public Collection<? extends TypeWrapper> getInnerTypes() {
        Class<?>[] declaredClasses = this.type.getDeclaredClasses();
        if (declaredClasses == null) {
            return Collections.emptyList();
        }
        List<TypeWrapper> list = new ArrayList<TypeWrapper>(declaredClasses.length);
        for (int i = 0; i < declaredClasses.length; i++) {
            list.add(wrapperFor(declaredClasses[i]));
        }
        return list;
    }

    /**
     * {@inheritDoc}
     */
    public TypeWrapper getEnclosingType() {
        return this.type.getEnclosingClass() == null ? null : wrapperFor(this.type.getEnclosingClass());
    }

}
